package game;

import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.StringTokenizer;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JPanel;

/**
 * Loads and stores images.
 * Information about which images to load could be given when creating an ImagesLoader object.
 * Images could be loaded with it's public methods.
 */
public class ImagesLoader {

	private final static String IMAGE_DIR = "pictures/";
	
	private HashMap<String, ArrayList<BufferedImage>> imagesMap;
	
	private HashMap<String, ArrayList<String>> gNamesMap;
	
	private GraphicsConfiguration gc;
	
	/**
	 * Constructs a new image loader and loads the images in the given file.
	 * 
	 * @param fnm The name of the file containing information about the images to load.
	 */
	public ImagesLoader(String fnm){
		initLoader();
		loadImagesFile(fnm);
	}
	
	/**
	 * Constructs a new image loader.
	 */
	public ImagesLoader(){
		initLoader();
	}
	
	/**
	 *  Initiate the ImagesLoader.
	 */
	private void initLoader(){
		imagesMap = new HashMap<String, ArrayList<BufferedImage>>();
		gNamesMap = new HashMap<String, ArrayList<String>>();
		
		GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
		gc = ge.getDefaultScreenDevice().getDefaultConfiguration();
	}
	
	/**
	 * Load file containing information of images to load.
	 * 
	 * Formats:
	 * 		o <fnm>						// a single image
	 * 		n <fnm*.ext> <numbers>		// a numbered sequence of images
	 * 		s <fnm> <numbers>			// an images strip
	 * 		g <name> <fnm> [ <fnm> ]*	// a group of images
	 * 		r <fnm> <degrees>			// an image to rotate and store
	 * 
	 * @param fnm The name of the file containing information about images to load.
	 */
	private void loadImagesFile(String fnm){
		String imsFnm = IMAGE_DIR + fnm;
		try{
			BufferedReader br = new BufferedReader(new FileReader(imsFnm));
			String line;
			char ch;
			while((line = br.readLine()) != null){
				if(line.length() == 0)
					continue;
				
				ch = Character.toLowerCase(line.charAt(0));
				if(ch == '0')
					getFileNameImage(line);
				else if(ch == 'n')
					getNumberedImages(line);
				else if (ch == 's')
					getStripImages(line);
				else if(ch == 'g')
					getGroupImages(line);
				else
					System.out.println("Do not recognize line: " + line);
			}
			br.close();
		}
		catch(IOException e){
			System.out.println("Error reading file");
			System.out.println(e.getStackTrace());
		}
	}
	
	/**
	 * Load a single image.
	 * 
	 * @param line Line containing information about the image to load.
	 */
	private void getFileNameImage(String line){
		StringTokenizer tokens = new StringTokenizer(line);
		
		if(tokens.countTokens() != 2){
			System.out.println("Wrong number of arguments for " + line);
		}else{
			tokens.nextToken();		// skip command label
			loadSingleImage(tokens.nextToken());
		}
	}
	
	/**
	 * Load a single image
	 * 
	 * @param fnm The name of the image to load.
	 * 
	 * @return Returns true if image is loaded, false otherwise.
	 */
	public boolean loadSingleImage(String fnm){
		String name = getPrefix(fnm);
		
		if(imagesMap.containsKey(name)){
			System.out.println("Error: " + name + " already used");
			return false;
		}
		
		BufferedImage bi = loadImage(fnm);
		if(bi != null){
			ArrayList<BufferedImage> imsList = new ArrayList<BufferedImage>();
			imsList.add(bi);
			imagesMap.put(name, imsList);
			return true;
		}else{
			return false;
		}
	}
	
	/**
	 * Extract the name of the image before it's file extension.
	 * 
	 * @param fnm Full name of file.
	 * 
	 * @return Returns the name of the file without it's file extension.
	 */
	private String getPrefix(String fnm){
		int posn;
		if((posn = fnm.lastIndexOf(".")) == -1){
			System.out.println("No prefix found for fileName: " + fnm);
			return fnm;
		}else{
			return fnm.substring(0, posn);
		}
	}
	
	/**
	 * Load numbered image.
	 * 
	 * 
	 * @param line Line containing information about the image to load.
	 * Format: n <fnm*.ext> <number>
	 */
	private void getNumberedImages(String line){
		StringTokenizer tokens = new StringTokenizer(line);
		
		if(tokens.countTokens() != 3){
			System.out.println("Wrong number of arguments for " + line);
		}else{
			tokens.nextToken();   // skip command label
			String fnm = tokens.nextToken();
			int number = -1;
			try{
				number = Integer.parseInt(tokens.nextToken());
			}
			catch(NumberFormatException e){
				System.out.println("Number is incorrect for " + line);
				e.printStackTrace();
			}
			
			loadNumImages(fnm, number);
		}
	}
	
	/**
	 * Load numbered image.
	 * 
	 * @param fnm The filename argument in:
	 * 		 n <f*.ext> <number>
	 * @param number The number of images to load.
	 * @return Returns the number of images succesfully loaded.
	 */
	public int loadNumImages(String fnm, int number){
		String prefix = null;
		String postfix = null;
		int starPosn = fnm.lastIndexOf("*");   // find the '*'
		if(starPosn == -1){
			System.out.println("No '*' in filename: " + fnm);
			prefix = getPrefix(fnm);
		}else{   // treat the fnm as prefix + "*" + postfix
			prefix = fnm.substring(0, starPosn);
			postfix = fnm.substring(starPosn+1);
		}
		
		if(imagesMap.containsKey(prefix)){
			System.out.println("Error: " + prefix + " already used");
			return 0;
		}
		
		return loadNumImages(prefix, postfix, number);
	}
	
	/**
	 * Load a series of image files with the filename format:
	 * 			prefix + <i> + postfix
	 * where in ranes from 0 to number-1.
	 * 
	 * @param prefix The name of the file without it's file extension.
	 * @param postfix The file extension of the file.
	 * @param number The number of images to load.
	 * @return Returns the number of images succesfully loaded.
	 */
	private int loadNumImages(String prefix, String postfix, int number){
		String imFnm;
		BufferedImage bi;
		ArrayList<BufferedImage> imsList = new ArrayList<BufferedImage>();
		int loadCount = 0;
		
		if(number <= 0){
			System.out.println("Error: Number <= 0: " + number);
			imFnm = prefix + postfix;
			if((bi = loadImage(imFnm)) != null){
				loadCount++;
				imsList.add(bi);
			}
		}else{
			for(int i = 0; i < number; i++){
				imFnm = prefix + i + postfix;
				if((bi = loadImage(imFnm)) != null){
					loadCount++;
					imsList.add(bi);
				}
			}
		}
		
		if(loadCount == 0){
			System.out.println("No images loaded for " + prefix);
		}else{
			imagesMap.put(prefix, imsList);
		}
		
		return loadCount;
	}
	
	/**
	 * Load image strip.
	 * 
	 * Format:
	 * 		s <fnm> <number>
	 * 
	 * @param line Line containing information about the image to load.
	 */
	private void getStripImages(String line){
		StringTokenizer tokens = new StringTokenizer(line);
		
		if(tokens.countTokens() != 3){
			System.out.println("Error: Wrong number of arguments for " + line);
		}else{
			tokens.nextToken();   // skip command label
			String fnm = tokens.nextToken();
			int number = -1;
			try{
				number = Integer.parseInt(tokens.nextToken());
			}
			catch(NumberFormatException e){
				System.out.println("Number is incorrect for " + line);
				e.printStackTrace();
			}
			
			loadStripImages(fnm, number);
		}
	}
	
	/**
	 * Load image strip file <fnm>, holding <number> images.
	 * 
	 * @param fnm Full name of file containine image strip.
	 * @param number The number of images in the strip.
	 * @return Returns the number of images succesfully loaded.
	 */
	public int loadStripImages(String fnm, int number){
		String name = getPrefix(fnm);
		if(imagesMap.containsKey(name)){
			System.out.println("Error: " + name + " already used");
			return 0;
		}
		
		BufferedImage[] strip = loadStripImageArray(fnm, number);
		if(strip == null){
			return 0;
		}
		
		ArrayList<BufferedImage> imsList = new ArrayList<BufferedImage>();
		int loadCount = 0;
		for(int i = 0; i < strip.length; i++){
			loadCount++;
			imsList.add(strip[i]);
		}
		
		if(loadCount == 0){
			System.out.println("No images loaded for " + name);
		}else{
			imagesMap.put(name, imsList);
		}
		
		return loadCount;
	}
	
	/**
	 * Load a group of images.
	 * 
	 * Format:
	 * 		g <name> <fnm> [ <fnm> ]*
	 * 
	 * @param line Line containing information about the images to load.
	 */
	private void getGroupImages(String line){
		StringTokenizer tokens = new StringTokenizer(line);
		
		if(tokens.countTokens() < 3){
			System.out.println("Error: Wrong number of arguments for " + line);
		}else{
			tokens.nextToken();		// skip command label
			String name = tokens.nextToken();
			
			ArrayList<String> fnms = new ArrayList<String>();
			fnms.add(tokens.nextToken());
			while(tokens.hasMoreTokens()){
				fnms.add(tokens.nextToken());
			}
			
			loadGroupImages(name, fnms);
		}
	}
	
	/**
	 * Load a group of images whose filenames are stored in the ArrayList <fnms>.
	 * They will be stored under the 'g' name <name>.
	 * 
	 * @param name The name which the images are stored under.
	 * @param fnms The ArrayList containing the filenames of the images.
	 * @return Returns the number of succesfully loaded images.
	 */
	public int loadGroupImages(String name, ArrayList<String> fnms){
		if(imagesMap.containsKey(name)) {
			System.out.println("Error : " + name + " already used");
			return 0;
		}
		
		if(fnms.size() == 0){
			System.out.println("Error: List of filenames is empty. ");
			return 0;
		}
		
		BufferedImage bi;
		ArrayList<String> nms = new ArrayList<String>();
		ArrayList<BufferedImage> imsList = new ArrayList<BufferedImage>();
		String nm, fnm;
		int loadCount = 0;
		
		for(int i = 0; i < fnms.size(); i++){	// load the files
			fnm = fnms.get(i);
			nm = getPrefix(fnm);
			if((bi = loadImage(fnm)) != null){
				loadCount++;
				imsList.add(bi);
				nms.add(nm);
			}
		}
		
		if(loadCount == 0){
			System.out.println("No images loaded for " + name);
		}else{
			imagesMap.put(name, imsList);
			gNamesMap.put(name, nms);
		}
		
		return loadCount;
	}
	
	/**
	 * Supply the group filenames in an array.
	 * 
	 * @param name The name of the grouped images.
	 * @param fnms The filenames of the images.
	 * @return Returns The number of succesfully loaded images.
	 */
	public int loadGroupImages(String name, String[] fnms){
		ArrayList<String> al = new ArrayList<String>(Arrays.asList(fnms));
		return loadGroupImages(name, al);
	}
	
	/**
	 * Access method for a single image.
	 * Get the image associated with <name>. If there are several images
	 * stored under that name, return teh first one in the list.
	 * 
	 * @param name The name which the images are stored under.
	 * @return Returns the first image associated with <name>.
	 */
	public BufferedImage getImage(String name){
		ArrayList<BufferedImage> imsList = imagesMap.get(name);
		if(imsList == null){
			System.out.println("No image(s) stored under " + name);
			return null;
		}
		
		return imsList.get(0);
	}
	
	/**
	 * Access method for a single image.
	 * Get the image associated with <name> at position <posn> in its list.
	 * If <posn> is < 0 then return the first image in the list.
	 * If <posn> is bigger than teh list's size, then calculate its value modulo the size.
	 * 
	 * @param name The name of the list containing the images.
	 * @param posn The position of the image to be returned.
	 * @return Returns the image <name> at position <posn>.
	 */
	public BufferedImage getImage(String name, int posn){
		ArrayList<BufferedImage> imsList = imagesMap.get(name);
		if(imsList == null){
			System.out.println("No image(s) stored under " + name);
			return null;
		}
		
		int size = imsList.size();
		if(posn < 0){
			return imsList.get(0);
		}
		else if(posn >= size){
			int newPosn = posn % size;
			return imsList.get(newPosn);
		}
		
		return imsList.get(posn);
	}
	
	/**
	 * Access method for a single image.
	 * Get the image associated with the group <name> and filename prefix <fnmPrefix>.
	 * 
	 * @param name The name of the group of images.
	 * @param fnmPrefix The filename prefix of the searched image.
	 * @return Returns the image with name <fnmPrefix> in group <name>.
	 */
	public BufferedImage getImage(String name, String fnmPrefix){
		ArrayList<BufferedImage> imsList = imagesMap.get(name);
		if(imsList == null){
			System.out.println("No image(s) stored under " + name);
			return null;
		}
		
		int posn = getGroupPosition(name, fnmPrefix);
		if(posn < 0){
			return imsList.get(0);
		}
		
		return imsList.get(posn);
	}
	
	/**
	 * Access method for the position of the image <fnmPrefix> in the group <name>
	 * of images in the hashmap gNamesMap.
	 * Search the hashmap entry for <name>, looking for <fnmPrefix>.
	 * 
	 * @param name The name of the group of images.
	 * @param fnmPrefix The name of the image searched for in the group.
	 * @return Returns the position of the image <fnmPrefix> in the group <name>.
	 */
	private int getGroupPosition(String name, String fnmPrefix){
		ArrayList<String> groupNames = gNamesMap.get(name);
		if(groupNames == null){
			System.out.println("No group names for " + name);
			return -1;
		}
		
		String nm;
		for(int i = 0; i < groupNames.size(); i++){
			nm = groupNames.get(i);
			if(nm.equals(fnmPrefix)){
				return i;		// the position of <fnmPrefix> in the list of names
			}
		}
		
		return -1;
	}
	
	/**
	 * Access method for a list of all images stored under <name>.
	 * 
	 * @param name The name of the list of images.
	 * @return Returns the list stored under <name>.
	 */
	public ArrayList<BufferedImage> getImages(String name){
		ArrayList<BufferedImage> imsList = imagesMap.get(name);
		if(imsList == null){
			System.out.println("No image(s) stored under " + name);
			return null;
		}
		
		return imsList;
	}
	
	/**
	 * Test if <name> is a key in the hashmap imagesMap.
	 * 
	 * @param name The name of the key.
	 * @return Returns true if images are stored under the name <name>, false otherwise.
	 */
	public boolean isLoaded(String name){
		return imagesMap.get(name) != null;
	}
	
	/**
	 * Acces method for the number of images stored under the name <name>.
	 * 
	 * @param name The name of the list containing the images.
	 * @return Returns the number of images.
	 */
	public int numImages(String name){
		ArrayList<BufferedImage> imsList = imagesMap.get(name);
		return imsList != null ? imsList.size() : 0;
	}
	
	/**
	 * Load a single image.
	 * 
	 * @param fnm The name of the image to laod.
	 * @return Returns the image as a BufferedImage which is compatible
	 * with the graphics device being used.
	 */
	public BufferedImage loadImage(String fnm){
		try{
			BufferedImage im = ImageIO.read(getClass().getResource(IMAGE_DIR + fnm));
			int transparancy = im.getColorModel().getTransparency();
			BufferedImage copy = gc.createCompatibleImage(
									im.getWidth(), im.getHeight(), transparancy);
			Graphics2D g2d = copy.createGraphics();
			
			g2d.drawImage(im, 0, 0, null);
			g2d.dispose();
			return copy;
		}
		catch(IOException e){e.printStackTrace(); return null;}
	}
	
	/**
	 * Load a single image.
	 * 
	 * @param fnm The name of the file.
	 * @return Returns the image as a BufferedImage.
	 */
	public BufferedImage loadImage2(String fnm){
		ImageIcon imIcon = new ImageIcon(getClass().getResource(IMAGE_DIR + fnm) );
		int width = imIcon.getIconWidth();
		int height = imIcon.getIconHeight();
		Image im = imIcon.getImage();
		
		return makeBIM(im, width, height);
	}
	
	/**
	 * Create a BufferedImage copy of an image.
	 * 
	 * @param im The image to create a copy of.
	 * @param width The width of the image.
	 * @param height The height of the image.
	 * @return Returns a BufferedImage copy of the image.
	 */
	private BufferedImage makeBIM(Image im, int width, int height){
		BufferedImage copy = new BufferedImage(width, height, 
		                                  BufferedImage.TYPE_INT_ARGB);
		// create a graphics context
		Graphics2D g2d = copy.createGraphics();
		
		// copy image
		g2d.drawImage(im,0,0,null);
		g2d.dispose();
		return copy;
	}
	
	/**
	 * Load a single image from file.
	 * 
	 * @param fnm The name of the file.
	 * @return Returns the image as a BufferedImage.
	 */
	public BufferedImage loadImage3(String fnm){
		Image im = readImage(fnm);
	    if (im == null)
	      return null;

	    int width = im.getWidth( null );
	    int height = im.getHeight( null );

	    return makeBIM(im, width, height);
	}

	/**
	 * Load a single image, waiting for it to be fully loaded.
	 * 
	 * @param fnm The name of the image to load.
	 * @return Returns the image.
	 */
	private Image readImage(String fnm){
		Image image = Toolkit.getDefaultToolkit().getImage(
	                     getClass().getResource(IMAGE_DIR + fnm) );
	    MediaTracker imageTracker = new MediaTracker( new JPanel() );
	
	    imageTracker.addImage(image, 0);
	    try {
	      imageTracker.waitForID(0);
	    }
	    catch (InterruptedException e) {
	      return null;
	    }
	    if (imageTracker.isErrorID(0)){
	    	return null;
	    }
	    return image;
	}
	
	/**
	 * Extract the individual images from the strip image file, <fnm>.
	 * Assuming the images are stored in a single row, and that there are <number>
	 * of them.
	 * 
	 * @param fnm The name of the file containing the strip image.
	 * @param number The number of images in the strip image.
	 * @return Returning the images as an array of BufferedImages.
	 */
	public BufferedImage[] loadStripImageArray(String fnm, int number){
	    if (number <= 0) {
	      System.out.println("number <= 0; returning null");
	      return null;
	    }

	    BufferedImage stripIm;
	    if ((stripIm = loadImage(fnm)) == null) {
	      System.out.println("Returning null");
	      return null;
	    }

	    int imWidth = (int) stripIm.getWidth() / number;
	    int height = stripIm.getHeight();
	    int transparency = stripIm.getColorModel().getTransparency();

	    BufferedImage[] strip = new BufferedImage[number];
	    Graphics2D stripGC;

	    // each BufferedImage from the strip file is stored in strip[]
	    for (int i=0; i < number; i++) {
	      strip[i] =  gc.createCompatibleImage(imWidth, height, transparency);
	       
	      // create a graphics context
	      stripGC = strip[i].createGraphics();
	      // stripGC.setComposite(AlphaComposite.Src);

	      // copy image
	      stripGC.drawImage(stripIm, 
	                  0,0, imWidth,height,
	                  i*imWidth,0, (i*imWidth)+imWidth,height,
	                  null);
	      stripGC.dispose();
	    } 
	    return strip;
	}
}
